在这一小节中,我们会分析 一个prefuse 程序示例: 一个简单的社交网络可视化。 我们的可视化程序会 将一个社交网络呈现出来 ,展现出一个根据力学规则控制 其布局的动画效果,节点 会按照性别来分配 不同颜色,并且 会显示出它们所代表的人的名字。 以下代码都是写在一个程序 类 的 main 方法里的。 此示例的源代码可在这里下载 Example.java 。
第一步, 将图数据载入 到一个prefuse 图 ( Graph ) 实例中。首先观察 一下 这个文件的内容: socialnet.xml (这个文件 也可在prefuse 发行包里的 data 目录中找到)。注意 ,那些节点有两个数据属性: 一个 name 字段和一个 gender 字段。 以下代码展示了如何使用 GraphMLReader 类( GraphReader 类的一个实例 )来将GraphML 格式 的文件 socialnet.xml 载入到一个prefuse Graph 数据结构中 。
Graph graph = null;
try {
graph = new GraphMLReader().readGraph("/socialnet.xml");
} catch ( DataIOException e ) {
e.printStackTrace();
System.err.println("Error loading graph. Exiting...");
System.exit(1);
}
这段代码尝试载入这个图,如果出错 了, 则会给出错误 码(任何 不等于0的退出码 都表示出错 )并且退出。 这里详细说一下 readGraph 方法 的参数。 为什么这里传个 "/socialnet.xml" 就行了? 这是因为数据读取器尝试 用多种方法来解释 这个文件名。首先, 它检查一下这个字符串是不是一个URL (例如, 以 http:// 、 ftp:// 或 file:// 开头 ),如果是的话,则打开一个URL连接。如果 不是, 它检查一下这个字符串是不是指向Java 运行 时类路径(classpath)里的某个资源。由于prefuse 发行包里的 data 目录已经包含在类路径里了,所以 "/socialnet.xml"应当能够正常载入 的 --它位于类路径的根目录中。最终 ,如果 这个字符串不能被解释成 类路径中的一个资源, 则它会被当成文件系统 中某个文件的路径。
如果妳在载入文件时有问题, 则把prefuse 的data 目录添加到类路径中,或者改变文件 的位置。例如,改成 http://prefuse.org/doc/manual/intro/socialnet.xml 就没问题了。无论如何 ,最终结果 就是socialnet.xml 文件 的内容被读取到图中去了。
现在我们需要为这个图创建一个可视化抽象 层 。 要做到这一点, 我们首先创建 一个新的 Visualization 实例,并且 将图数据添加到其中。 我们将刚载入的图注册为数据分组名字“graph”。稍后 会使用这个数据分组名字来访问这些可视化数据。 当妳将 Graph 或Tree 实例添加到可视化对象时, 会同时自动注册另两个 子组: 一个是节点集合(加上 一个后缀 ".nodes"),另一个是边的集合(加上 一个后缀 ".edges")。 在这个示例中,稍后会看到怎么使用子组。
// 将此图加入到可视化对象中,设置分组 名为"graph"
// 节点和边的集合可通过"graph.nodes"和"graph.edges"访问
Visualization vis = new Visualization();
vis.add("graph", graph);
下一步,设置渲染 器( Renderers ),它们 将用来绘制可视 化对象中包含 的可视化条目( VisualItems )。默认情况 下, 可视化对象已经包含了一个 DefaultRendererFactory 实例, 它会使用 一个 EdgeRenderer (默认 会绘制直线 的边 )来绘制所有的EdgeItems,使用 一个 ShapeRenderer ( 它会将条目绘制成基本形状,例如□和△ )来绘制所有其它的条目。由于 我们想要看到节点 中出现文本标签,所以 我们创建 一个新的 LabelRenderer ,并且告诉它使用"name" 这个数据字段来作为标签 的文字内容。 我们还告诉渲染器要给文字标签绘制一个圆角 , 这样看起来更光滑。然后 我们创建一个新的DefaultRendererFactory, 它会 将刚才新建的文本标签渲染器作为所有 非边条目的默认渲染器(所有 的 EdgeItems 会使用之前提到的默认 EdgeRenderer 来渲染 )。 DefaultRendererFactory还有狠多其它的功能,我们目前没有用到--我们也可以设置默认的边渲染器,还可以添加任意数量 的规则 来控制渲染 器的分配(参考编程接口文档)。
// 针对NodeItems,绘制其"name"标签
LabelRenderer r = new LabelRenderer("name");
r.setRoundedCorner(8, 8); // 圆角
// 创建一个新的默认渲染器工厂
// 针对所有的非EdgeItems条目,返回我们的名字标签渲染器
// 针对EdgeItems,包含默认的直线边渲染器
vis.setRendererFactory(new DefaultRendererFactory(r));
现在我们可以设置可视化编码了。创建 Action 模块 ,它们会处理可视 化对象中的 可视化条目 。 我们首先创建一些颜色动作(ColorActions)。默认情况 下,每个可视化条目支持三个颜色值:边框 、填充和文字颜色。边框颜色 指的是线条和轮廓的颜色,填充颜色 是指条目的内部颜色,文字颜色 是指任何 的文字和标签的颜色。所有颜色 的默认值都是完全的透明色,所以默认情况下 不会绘制任何东西。 在prefuse 中,每个颜色 值都被编码为单个整数, 它表示的是RGBA(red-green-blue-alpha)值,其中每个分量的范围是0到255 。Alpha表示的是透明度,0表示完全透明,255表示完全不透明。 ColorLib 类提供了一些好用的方法,可用来创建颜色值。
要想 为社交网络中的人根据性别设置不同颜色的话, 我们首先创建一个自定义的调色板。 这其实是一个由允许的颜色值组成的数组。 在这个示例中, 我们创建 一个数组,其中粉红代表女性,婴儿蓝代表男性。然后 我们创建一个 DataColorAction , 它会计算颜色的分配。DataColorAction 的构造函数需要以下参数
•. 要处理的数据分组的名字( 在这个示例中就是 graph.nodes )
•. 以之为基础进行编码的数据字段的名字( 在这个示例中是 gender )
•.字段的数据类型,可以是以下之一: NOMINAL (各类标签组成的集合)、 ORDINAL (有序表)或 NUMERICAL (数字)
•. 一个可选和调色板(如果 这里不指定调色板,则会创建一个默认的调色板 )。
关于顺序,有一点要注意:DataColorAction会 将NOMINAL 和 ORDINAL 数据的值按照它们的自然排序规则来排序,对于文本字符串 来说就是字母顺序。 这就是调色板 中粉红 色排在前面 的原因,女性英文female 的'F'位于男性英文male 的'M'前面。
类似地, 我们使用 ColorAction 组件来将节点 的文字设置成黑色,将 边的线颜色设置成 浅灰色。ColorActions可用来 为所有条目设置一个默认的颜色值, 但同样可支持任意数量的附加规则,以进行 更复杂的颜色分配。
最后,我们创建一个 ActionList 实例, 它将所有 的颜色分配动作组合成一个单独的可执行单元。
// 创建我们的普通调色板
// 粉红色表示女性,婴儿蓝表示男性
int[] palette = new int[] {
ColorLib.rgb(255,180,180), ColorLib.rgb(190,190,255)
};
// 使用上面的调色板来将字面数据 值映射到颜色
DataColorAction fill = new DataColorAction("graph.nodes", "gender",
Constants.NOMINAL, VisualItem.FILLCOLOR, palette);
// 对于节点文字使用黑色
ColorAction text = new ColorAction("graph.nodes",
VisualItem.TEXTCOLOR, ColorLib.gray(0));
// 对于边使用浅灰色
ColorAction edges = new ColorAction("graph.edges",
VisualItem.STROKECOLOR, ColorLib.gray(200));
// 创建一个动作列表,包含所有的颜色分配
ActionList color = new ActionList();
color.add(fill);
color.add(text);
color.add(edges);
接下来,我们创建一个单独的动作列表(ActionList), 它提供一个 带 动画效果的布局。所有 的动作实例都可以 被配置为只运行一次(默认情况),或者在一段指定的时间里持续运行。通过 将持续时间的值设置成无限( INFINITY ),我们 可以让 这个ActionList 永远运行,这将引起布局持续 地更新。然后 ,我们添加一个基于力学的布局器( ForceDirectedLayout ), 由它来为图中的元素分配空间位置。 我们还添加一个 重绘动作( RepaintAction ), 以便在布局被重新计算时告之对应 的显示对象要重绘。
// 创建一个动作列表,它实现 了带动画效果的布局
// INFINITY参数告诉动作列表要永远运行
ActionList layout = new ActionList(Activity.INFINITY);
layout.add(new ForceDirectedLayout("graph"));
layout.add(new RepaintAction());
现在我们将这两个动作列表添加到可视化对象中。 这将导致那些动作被正确地注册到这个可视化对象中,稍后它们 被调用的时候 就能访问这个可视化对象了。每个注册 的动作也会被赋予一个唯一的名字,以便日后调用。
// 将动作添加到可视化对象中
vis.putAction("color", color);
vis.putAction("layout", layout);
我们还需要为这些可视化的数据创建一个显示对象( Display )。 这里创建一个 新的显示对象实例, 它 被配置为 从可视化对象中拉取所有的可视化条目( 妳 还可以提供一个断言( Predicate )对象, 它控制的是 这个显示对象会处理哪些条目 )。 我们还 为这个显示对象设置一个适当 的尺寸,以像素为单位。然后 ,我们向显示对象中添加3个交互式控制器: Display:
•. 一个 DragControl , 可使用鼠标左键来 将可视 化条目拖动
•. 一个 PanControl , 可使用鼠标左键抓住显示对象的空白位置 来拖动整个显示区域
•. 一个 ZoomControl , 可使用鼠标右键 上下拖动来缩放 这个显示对象
我们使用这些控制器的默认设置。 可以使用对应 的构造函数来改变触发 这些控制器的鼠标按钮和其它设置。
// 创建一个新的显示对象, 它为从我们的可视化对象中拉取数据
Display display = new Display(vis);
display.setSize(720, 500); // 设置显示对象的尺寸
display.addControlListener(new DragControl()); // 拖动条目
display.addControlListener(new PanControl()); // 抓住背景就可以拖动整个显示对象
display.addControlListener(new ZoomControl()); // 右键上下拖动就可以缩放
快要完成了!剩下 的事情就是将这个显示对象添加 到一个新的程序窗口中,再让东西都运行起来。 我们创建一个新的 JFrame 实例(Java Swing 用户界面开发 包中的顶级窗口类 ) ,设置 好它的标题, 让程序在窗口被关闭时退出。然后添加 我们的显示对象, “填充”(pack)窗口(确保窗口 中的部件 -- 在这个示例中只有我们的显示对象一个部件 -- 被正确地布局了 ),显示出窗口。最后 , 我们运行颜色分配动作列表,确保每个条目 都被分配了适合的颜色,然后 让那个永远运行的布局动作列表开始运行。
// 创建一个新的窗口,用来 装下这个可视化对象
JFrame frame = new JFrame("prefuse example");
// 确保程序在窗口被关闭时退出
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(display);
frame.pack(); // 在窗口中将组件布局起来
frame.setVisible(true); // 显示窗口
vis.run("color"); // 分配颜色
vis.run("layout"); // 启动动画布局
妳可以下载完整的示例文件 Example.java 。 在妳把这个示例运行起来之后, 试着替换 掉其中的一些组件。例如, 妳可以去掉INFINITY 参数 ,让那个负责布局的动作列表只运行一次,然后尝试 一下 这个库里提供的 其它 图布局器 。
另外,prefuse 发行包的 demos 目录里有狠多其它类型 的示例程序。如果 妳消化 了以上的这个示例,那么妳可以去研究一下其它示例了。
HxLauncher: Launch Android applications by voice commands